package net.sf.eclipseshell.clojureenvironment.providers;

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import net.sf.eclipseshell.EclipseShellPlugin;
import net.sf.eclipseshell.extensions.IExecutionEnvironment;
import net.sf.eclipseshell.extensions.IJavaEnvironment;
import net.sf.eclipseshell.extensions.IJavaRuntimeHandler;
import net.sf.eclipseshell.extensions.IWorkspaceReloadHandler;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.ITextHover;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;

import clojure.lang.ArraySeq;
import clojure.lang.Compiler;
import clojure.lang.RT;
import clojure.lang.Symbol;
import clojure.lang.Var;

public class ClojureEclipseProvider implements IExecutionEnvironment, IJavaRuntimeHandler, IWorkspaceReloadHandler {



	private final IResource fFile;
	private final Writer fConsoleOutput;
	private PrintStream fConsoleOutputStream;
	private IJavaEnvironment fJavaEnvironment;

	static final Symbol USER = Symbol.create("user");
	static final Symbol CLOJURE = Symbol.create("clojure.core");

	static final Var in_ns = RT.var("clojure.core", "in-ns");
	static final Var refer = RT.var("clojure.core", "refer");
	static final Var ns = RT.var("clojure.core", "*ns*");
	static final Var compile_path = RT.var("clojure.core", "*compile-path*");
	static final Var warn_on_reflection = RT.var("clojure.core", "*warn-on-reflection*");
	static final Var print_meta = RT.var("clojure.core", "*print-meta*");
	static final Var print_length = RT.var("clojure.core", "*print-length*");
	static final Var print_level = RT.var("clojure.core", "*print-level*");
	static final Var star1 = RT.var("clojure.core", "*1");
	static final Var star2 = RT.var("clojure.core", "*2");
	static final Var star3 = RT.var("clojure.core", "*3");
	static final Var stare = RT.var("clojure.core", "*e");

	public ClojureEclipseProvider(IResource file, OutputStream consoleOutput,String mode) {
		this.fFile = file;
		this.fConsoleOutput = new OutputStreamWriter(consoleOutput);
		this.fConsoleOutputStream = new PrintStream(consoleOutput);
		try {
			new ClojureReloader().launch(file, this);
		} catch (CoreException e) {
			e.printStackTrace(fConsoleOutputStream);
		}
		// TODO: check if this messes up the general ns
		evaluateClojureCode(
				"(ns eclipseshell-clojure)\n" +
				"(defn symbol-to-doc-vectors [] " +
				"  (sort-by (comp first) (map (fn [x]"+
				   "[(get (meta (second x)) :name )  (get (meta (second x)) :doc ) ] )"+   
					 "(ns-publics 'clojure.core  )))"+
					 ")\n"
				   );
		
	}

	public String execute(String code) {
		evaluateClojureCode(code);
		return "HELLO";
	}

	private Object evaluateClojureCode(String code) {
		Var.pushThreadBindings(
				RT.map(ns, ns.get(),
				       warn_on_reflection, warn_on_reflection.get(),
				       print_meta, print_meta.get(),
				       print_length, print_length.get(),
				       print_level, print_level.get(),
				       compile_path, "classes",
				       star1, null,
				       star2, null,
				       star3, null,
				       stare, null));


		
		try{
			in_ns.invoke(USER);
			refer.invoke(CLOJURE);
			
			Object ret = Compiler.load(new StringReader(code));
			RT.print(ret, fConsoleOutput);

			fConsoleOutput.write('\n');
			fConsoleOutput.flush();
			return ret;
		} catch(Exception e){
			e.printStackTrace(fConsoleOutputStream);
		}
		return null;
	}

	public String execute(Reader codeReader) {
		// TODO: implement
		return "NOP";
	}

	public IContentAssistProcessor getContentAssistProcessor(IProject proj) {
		return new IContentAssistProcessor(){
			public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
//				String allSyms = "(sort-by (comp first) (map (fn [x]"+
//						   "[(get (meta (second x)) :name )  (get (meta (second x)) :doc ) ] )"+   
//						 "(ns-publics 'clojure  )))";
				String allSyms = "(eclipseshell-clojure/symbol-to-doc-vectors)";
				try {
					Object eval = evaluateClojureCode(allSyms);
					if (eval instanceof Collection) {
						Collection coll = (Collection) eval;
						// NOTE: yeah - ugly; but I want to sort the symbols ... and since IPersistentMap 
						ICompletionProposal[] icp = new ICompletionProposal[coll.size()];
						
						int i = 0;
						
						ArraySeq x = (ArraySeq) eval;
						
						
						List tmp = Arrays.asList(x.toArray());
						List sortedKeys = new ArrayList(tmp.size());
						for (Iterator iterator = tmp.iterator(); iterator.hasNext();) {
							List currEntry = (List) iterator.next();
							sortedKeys.add(new Object[] { currEntry.get(0).toString(),currEntry.get(1)  });
						}


						for ( Iterator iterator = sortedKeys.iterator(); iterator.hasNext();i++) {
							Object[] currBinding = (Object[]) iterator.next();
							String currSym = currBinding[0].toString();
							Object docString = currBinding[1];
							if(docString == null){
								docString = "";
							}
							icp[i] = new CompletionProposal(currSym, offset, currSym.length(), offset +  currSym.length(), null, currSym, null, docString.toString());
						}
						// TODO: integrate with JDT for obvious Java types
//						fJavaEnvironment.addMethodCompletionForClass(offset, retValueList, prefix, typeDescriptor, lastElement)
						return icp;
					}
				} catch (Exception e) {
					e.printStackTrace(ClojureEclipseProvider.this.fConsoleOutputStream);
				}
				return null;
			}
			public IContextInformation[] computeContextInformation(
					ITextViewer viewer, int offset) {
				return null;
			}
			public char[] getCompletionProposalAutoActivationCharacters() {
				return null;
			}
			public char[] getContextInformationAutoActivationCharacters() {
				return null;
			}
			public IContextInformationValidator getContextInformationValidator() {
				return null;
			}
			public String getErrorMessage() {
				return null;
			}
			
		};
	}

	public Object[] getDamagerAndRepairer() {
		return null;
	}

	public ITextHover getTextHover(IProject proj) {
		return null;
	}

	public void setName(String string) {

	}

	public void shutdown() {
		Var.popThreadBindings();
	}

	public void reload(IFile file) {
		String fileName = file.getRawLocation().toOSString();
		try {
			// TODO: add classpath of /lib's project (if it's a JDT project);
			// use Clojure's add-classpath for Clojure in Eclipse env;

			Compiler.loadFile(fileName);
		} catch (Exception e) {
			e.printStackTrace(fConsoleOutputStream);
		}

	}

	public void setJavaEnvironment(IJavaEnvironment env) {
		this.fJavaEnvironment = env;
	}

}

class ClojureReloader implements IResourceChangeListener {

	IWorkspaceReloadHandler reloader_;
	IProject project_;
	private ClojureEclipseProvider fEnvironment;


	public void launch(IResource res, ClojureEclipseProvider env) throws CoreException {

		this.fEnvironment = env;
//		TODO: build GUI tabs for launch config!!! 
		System.out.println("LaunchConfig: JRuby");


//		project_ = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
//		final IFile runFile = project_.getFile(fileLocation);

		
		if(res != null){
			project_ = res.getProject();
		} else {
			return;
//		   PlatformUI.getWorkbench().	
		}
		
		if(project_ != null){
			project_.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
		}

		try {
			final Job bgReload;
			if (fEnvironment instanceof IWorkspaceReloadHandler) {

				reloader_ = (IWorkspaceReloadHandler) fEnvironment;
				// TODO: iterate over all lib files!
				IFolder folder = project_.getFolder("lib");
				IResource[] resources = folder.members();
				List affectedChildren = Arrays.asList(resources);

				
				
				bgReload = new ReloadJob("Reload", affectedChildren);
				bgReload.schedule();
//				for (int i = 0; i < resources.length; i++) {
//				IResource libraryFile = resources[i];
//				reloader_.reload(project_.getFile(libraryFile.getName()));
//				}

			} else {
				bgReload = null;
			} 


		} catch (CoreException e) {
			EclipseShellPlugin.getDefault().log(IStatus.ERROR, e.getMessage(), e);
		} 

	}

	public void resourceChanged(IResourceChangeEvent event) {
		IResourceDelta delta = event.getDelta();

		final List affectedChildren = new ArrayList();
		try {
			delta.accept(new IResourceDeltaVisitor(){
				public boolean visit(IResourceDelta delta) throws CoreException {
					IResource resource = delta.getResource();
					if(resource.getName().endsWith("cl")||resource.getName().endsWith("cli")){
						affectedChildren.add(resource);
					}
					return true;
				}

			});
		} catch (CoreException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("Resource changed!");
//		TODO: Critical: put this on a bg JOB!
//		TODO: always require 'java'
		Job bgReload = new ReloadJob("Reload", affectedChildren);
		bgReload.schedule();

	}

	protected void finalize() throws Throwable {
//		make sure that launch resource listeners don't leak
		ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
	}
	public class ReloadJob extends Job{
		private List files_;
		public ReloadJob(String name, List files){
			super(name);
			files_ = files;
		}
		protected IStatus run(IProgressMonitor monitor) {
			monitor.beginTask("Reloading", files_.size());
			int work=0;
			for (Iterator iter = files_.iterator(); iter.hasNext();work++) {
				IResource res = (IResource) iter.next();			
				if (res instanceof IFile) {
					IFile file = (IFile) res;
					System.out.println("Reload: " + file);
					try{
						reloader_.reload(file);
					} catch (Exception e) {
						System.out.println("File: " + res.toString()+ "\n" +e.getMessage() + "\n" + e );
						EclipseShellPlugin.getDefault().log(Status.ERROR, "File: " + res.toString()+ "\n" +e.getMessage() + "\n" + e, e);
					}
				}
				monitor.worked(work);
			}
			monitor.done();
			return Status.OK_STATUS;
		}

	};

}






